Zoom and pan images

revision:


With a lightbox in new page

Click an image to open the lightbox; wheel and drag to pan while enlarged.

code:
            <div class="gallery" id="gallery">
                <div><img src="../images/1.jpg" class="image" alt="holiday" width="100%" height="100%"/></div>
                <div><img src="../images/2.jpg" class="image" alt="holiday" width="100%" height="100%"/></div>
                <div><img src="../images/3.jpg" class="image" alt="holiday" width="100%" height="100%"/></div>
                <div><img src="../images/4.jpg" class="image" alt="holiday" width="100%" height="100%"/></div>
                <div><img src="../images/5.jpg" class="image" alt="holiday" width="100%" height="100%"/></div>
                <div><img src="../images/1a.jpg" class="image" alt="holiday" width="100%" height="100%"/></div>
                <div><img src="../images/2a.jpg" class="image" alt="holiday" width="100%" height="100%"/></div>
                <div><img src="../images/3a.jpg" class="image" alt="holiday" width="100%" height="100%"/></div>
                <div><img src="../images/4a.jpg" class="image" alt="holiday" width="100%" height="100%"/></div>
                <div><img src="../images/5a.jpg" class="image" alt="holiday" width="100%" height="100%"/></div>
            </div>
            <div id="lightbox">
                <span id="close">×</span>
                <div id="zoom-container">
                    <img id="zoom-image" src="" alt="Zoomable">
                </div>
            </div>
            <style>
                /* Gallery */
                .gallery { display: flex; flex-wrap: wrap; gap: 0.8vw; padding: 1.2vw; max-width: 90VW;  margin: 0 auto; width: 90vw;}
                .gallery img { width: 16vw; height: 14vw;  object-fit: cover; cursor: pointer; border: 0.1vw solid #ddd;border-radius: 0.3vw; transition: border-color 0.2s;}
                .gallery img:hover { border-color: #007bff;}
                /* Lightbox */
                #lightbox { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.9); z-index: 1000; justify-content: center;
                align-items: center;}
                #lightbox.active {display: flex;}
                #zoomContainer { width: 50vw; height: 50vh; overflow: hidden; position: relative; cursor: grab;}
                #zoomContainer.dragging {cursor: grabbing;}
                #zoomImage {position: absolute; top: 0; left: 0; transition: transform 0.1s ease; user-select: none; max-width: none; max-height: none; }
                /* Close button */
                #close {position: absolute; top: 2vw; right: 3vw;color: white;font-size: 3vw; cursor: pointer; }
            </style>
            <script>
                // Get DOM elements
                const lightbox = document.getElementById('lightbox');
                const zoomImage = document.getElementById('zoom-image');
                const zoomContainer = document.getElementById('zoom-container');
                const closeBtn = document.getElementById('close');

                // Zoom/pan state
                let scale = 1;
                let offsetX = 0;
                let offsetY = 0;
                let isDragging = false;
                let startX, startY, startOffsetX, startOffsetY;

                const minScale = 1;
                const maxScale = 5;
                const zoomIntensity = 0.1;

                // Attach click listeners to all gallery images
                document.querySelectorAll('.gallery div img').forEach(thumb => {
                    thumb.addEventListener('click', () => {
                    const fullSrc = thumb.dataset.full || thumb.src;
                    openLightbox(fullSrc);
                    });
                });

                // Open lightbox
                function openLightbox(src) {
                    zoomImage.src = src;
                    scale = 1;
                    offsetX = 0;
                    offsetY = 0;
                    lightbox.classList.add('active');
                    setTimeout(updateTransform, 10);
                }

                // Close lightbox
                closeBtn.addEventListener('click', () => {
                    lightbox.classList.remove('active');
                });

                // Prevent closing when clicking image
                zoomContainer.addEventListener('click', (e) => e.stopPropagation());

                // Drag to pan
                zoomContainer.addEventListener('mousedown', (e) => {
                    if (e.button !== 0) return;
                    isDragging = true;
                    zoomContainer.classList.add('dragging');
                    startX = e.clientX;
                    startY = e.clientY;
                    startOffsetX = offsetX;
                    startOffsetY = offsetY;
                    e.preventDefault();
                });

                window.addEventListener('mousemove', (e) => {
                    if (!isDragging || !lightbox.classList.contains('active')) return;
                    offsetX = startOffsetX + (e.clientX - startX);
                    offsetY = startOffsetY + (e.clientY - startY);
                    updateTransform();
                });

                window.addEventListener('mouseup', () => {
                    isDragging = false;
                    zoomContainer.classList.remove('dragging');
                });

                // Zoom with wheel
                zoomContainer.addEventListener('wheel', (e) => {
                    if (!lightbox.classList.contains('active')) return;
                    e.preventDefault();
                    
                    const zoom = e.deltaY > 0 ? -zoomIntensity : zoomIntensity;
                    const newScale = Math.min(Math.max(scale + zoom, minScale), maxScale);
                    if (newScale === scale) return;

                    const rect = zoomContainer.getBoundingClientRect();
                    const mouseX = e.clientX - rect.left;
                    const mouseY = e.clientY - rect.top;

                    offsetX = (mouseX - offsetX) * (newScale / scale) + offsetX - mouseX;
                    offsetY = (mouseY - offsetY) * (newScale / scale) + offsetY - mouseY;

                    scale = newScale;
                    updateTransform();
                });

                // Double-click to zoom
                zoomContainer.addEventListener('dblclick', (e) => {
                    if (!lightbox.classList.contains('active')) return;
                    
                    const rect = zoomContainer.getBoundingClientRect();
                    const mouseX = e.clientX - rect.left;
                    const mouseY = e.clientY - rect.top;

                    const targetScale = scale >= 2 ? minScale : 2;
                    offsetX = (mouseX - offsetX) * (targetScale / scale) + offsetX - mouseX;
                    offsetY = (mouseY - offsetY) * (targetScale / scale) + offsetY - mouseY;

                    scale = targetScale;
                    updateTransform();
                });

                // Apply transform & constrain panning
                function updateTransform() {
                    if (!zoomImage.naturalWidth) return;

                    const imgW = zoomImage.naturalWidth;
                    const imgH = zoomImage.naturalHeight;
                    const contW = zoomContainer.clientWidth;
                    const contH = zoomContainer.clientHeight;

                    const maxOffsetX = (imgW * scale - contW) / 2;
                    const maxOffsetY = (imgH * scale - contH) / 2;

                    offsetX = Math.max(-maxOffsetX, Math.min(maxOffsetX, offsetX));
                    offsetY = Math.max(-maxOffsetY, Math.min(maxOffsetY, offsetY));

                    zoomImage.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
                }

                // Update when image loads
                zoomImage.onload = updateTransform;
            </script>
        

Without a lightbox

wheel and drag to pan while enlarged.

holiday
holiday
holiday
holiday
holiday
holiday
holiday
holiday
holiday
holiday
code:
            <div class="gallery1" id="gallery1">
                <div class="image-container"><img src="../images/1.jpg" class="zoomable-image" alt="holiday" width="100%" height="100%"/></div>
                <div class="image-container"><img src="../images/2.jpg" class="zoomable-image" alt="holiday" width="100%" height="100%"/></div>
                <div class="image-container"><img src="../images/3.jpg" class="zoomable-image" alt="holiday" width="100%" height="100%"/></div>
                <div class="image-container"><img src="../images/4.jpg" class="zoomable-image" alt="holiday" width="100%" height="100%"/></div>
                <div class="image-container"><img src="../images/5.jpg" class="zoomable-image" alt="holiday" width="100%" height="100%"/></div>
                <div class="image-container"><img src="../images/1a.jpg" class="zoomable-image" alt="holiday" width="100%" height="100%"/></div>
                <div class="image-container"><img src="../images/2a.jpg" class="zoomable-image" alt="holiday" width="100%" height="100%"/></div>
                <div class="image-container"><img src="../images/3a.jpg" class="zoomable-image" alt="holiday" width="100%" height="100%"/></div>
                <div class="image-container"><img src="../images/4a.jpg" class="zoomable-image" alt="holiday" width="100%" height="100%"/></div>
                <div class="image-container"><img src="../images/5a.jpg" class="zoomable-image" alt="holiday" width="100%" height="100%"/></div>
            </div>
            <style>
                /* Gallery */
                .gallery1 { display: flex; flex-wrap: wrap; gap: 0.8vw; padding: 1.2vw; max-width: 90VW;  margin: 0 auto; width: 90vw;}
                .image-container {width: 16vw; height: 14vw; overflow: hidden; border: 0.1vw solid #ccc; position: relative; cursor: grab;}
                .image-container.dragging {cursor: grabbing;}
                .zoomable-image {position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; transition: transform 0.1s ease; user-select: none;
                /* We'll control scale/position via JS */}
            </style>
            <script>
                // For each image container, attach zoom/pan behavior
                document.querySelectorAll('.image-container').forEach(container => {
                    const img = container.querySelector('.zoomable-image');

                    let scale = 1;
                    let offsetX = 0;
                    let offsetY = 0;
                    let isDragging = false;
                    let startX, startY, startOffsetX, startOffsetY;

                    const minScale = 1;
                    const maxScale = 3;
                    const zoomIntensity = 0.1;

                    // Initial style
                    img.style.transform = 'translate(0,0) scale(1)';

                    // Mouse down to start drag
                    container.addEventListener('mousedown', (e) => {
                        if (e.button !== 0) return; // left click only
                        isDragging = true;
                        container.classList.add('dragging');
                        startX = e.clientX;
                        startY = e.clientY;
                        startOffsetX = offsetX;
                        startOffsetY = offsetY;
                        e.preventDefault();
                    });

                    // Mouse move = pan
                    window.addEventListener('mousemove', (e) => {
                        if (!isDragging) return;
                        offsetX = startOffsetX + (e.clientX - startX);
                        offsetY = startOffsetY + (e.clientY - startY);
                        applyTransform();
                    });

                    // Mouse up = stop drag
                    window.addEventListener('mouseup', () => {
                        isDragging = false;
                        container.classList.remove('dragging');
                    });

                    // Zoom with wheel
                    container.addEventListener('wheel', (e) => {
                        e.preventDefault();
                        const zoom = e.deltaY > 0 ? -zoomIntensity : zoomIntensity;
                        const newScale = Math.min(Math.max(scale + zoom, minScale), maxScale);
                        if (newScale === scale) return;

                        // Zoom toward mouse position inside container
                        const rect = container.getBoundingClientRect();
                        const mouseX = e.clientX - rect.left;
                        const mouseY = e.clientY - rect.top;

                        // Adjust offset so point under cursor stays fixed
                        offsetX = (mouseX - offsetX) * (newScale / scale) + offsetX - mouseX;
                        offsetY = (mouseY - offsetY) * (newScale / scale) + offsetY - mouseY;

                        scale = newScale;
                        applyTransform();
                    });

                    // Optional: double-click to reset
                    container.addEventListener('dblclick', () => {
                        scale = 1;
                        offsetX = 0;
                        offsetY = 0;
                        applyTransform();
                    });

                    function applyTransform() {
                        // Get container and natural image dimensions
                        const contWidth = container.clientWidth;
                        const contHeight = container.clientHeight;
                        const imgWidth = img.naturalWidth;
                        const imgHeight = img.naturalHeight;

                        // Max allowable offset (so image doesn't show empty space)
                        const maxOffsetX = (imgWidth * scale - contWidth) / 2;
                        const maxOffsetY = (imgHeight * scale - contHeight) / 2;

                        // Constrain panning
                        offsetX = Math.max(-maxOffsetX, Math.min(maxOffsetX, offsetX));
                        offsetY = Math.max(-maxOffsetY, Math.min(maxOffsetY, offsetY));

                        // Apply
                        img.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
                    }

                    // Initialize on load
                    if (img.complete) {
                        applyTransform();
                    } else {
                        img.onload = applyTransform;
                    }
                });
            </script>
        

Zoom and pan images in-place

Click an image to enlarge it. Drag to pan while enlarged.

Note: Enlarging will push other content. Click enlarged image to shrink.

holiday
holiday
holiday
holiday
holiday
holiday
holiday
holiday
holiday
holiday
code:
        <div class="gallery2" id="gallery2">
            <div><img src="../images/1.jpg" class="interactive-img" alt="holiday" width="100%" height="100%"/></div>
            <div><img src="../images/2.jpg" class="interactive-img" alt="holiday" width="100%" height="100%"/></div>
            <div><img src="../images/3.jpg" class="interactive-img" alt="holiday" width="100%" height="100%"/></div>
            <div><img src="../images/4.jpg" class="interactive-img" alt="holiday" width="100%" height="100%"/></div>
            <div><img src="../images/5.jpg" class="interactive-img" alt="holiday" width="100%" height="100%"/></div>
            <div><img src="../images/1a.jpg" class="interactive-img" alt="holiday" width="100%" height="100%"/></div>
            <div><img src="../images/2a.jpg" class="interactive-img" alt="holiday" width="100%" height="100%"/></div>
            <div><img src="../images/3a.jpg" class="interactive-img" alt="holiday" width="100%" height="100%"/></div>
            <div><img src="../images/4a.jpg" class="interactive-img" alt="holiday" width="100%" height="100%"/></div>
            <div><img src="../images/5a.jpg" class="interactive-img" alt="holiday" width="100%" height="100%"/></div>
        </div>
        <style>
            .gallery2 {display: flex; flex-wrap: wrap; gap: 2vw; margin: 2vw 0;    }
            .interactive-img {/* Base style */ width: 16vw; height: 28vh; cursor: pointer; transition: width 0.3s ease, box-shadow 0.3s ease; position: relative; user-select: none; }
            .interactive-img.enlarged {width: 32vw; height: 38vh; /* or use max-width + auto height */ box-shadow: 0 0 15px rgba(0,0,0,0.3); z-index: 10; /* Optional: slight elevation */ margin: 0vw 0; }
            /* Instructions */
            .hint { color: #666; font-style: italic; margin-top: 0.8vw;}
        </style>
        <script>
            document.querySelectorAll('.interactive-img').forEach(img => {
            let isEnlarged = false;
            let positionX = 0;
            let positionY = 0;
            let isDragging = false;
            let startX, startY;

            // Toggle enlarge on click
            img.addEventListener('click', (e) => {
                if (!isEnlarged) {
                // Enlarge
                img.classList.add('enlarged');
                img.style.position = 'relative';
                img.style.cursor = 'grab';
                img.style.transform = 'translate(0, 0)';
                isEnlarged = true;
                } else {
                // Shrink back
                img.classList.remove('enlarged');
                img.style.cursor = 'pointer';
                img.style.transform = '';
                positionX = 0;
                positionY = 0;
                isEnlarged = false;
                }
                e.stopPropagation();
            });

            // Start drag
            img.addEventListener('mousedown', (e) => {
                if (!isEnlarged || e.button !== 0) return;
                isDragging = true;
                img.style.cursor = 'grabbing';
                startX = e.clientX;
                startY = e.clientY;
                e.preventDefault();
            });

            // Pan while dragging
            window.addEventListener('mousemove', (e) => {
                if (!isDragging || !isEnlarged) return;
                const dx = e.clientX - startX;
                const dy = e.clientY - startY;
                positionX += dx;
                positionY += dy;
                img.style.transform = `translate(${positionX}px, ${positionY}px)`;
                startX = e.clientX;
                startY = e.clientY;
            });

            // Stop drag
            window.addEventListener('mouseup', () => {
                if (isDragging) {
                isDragging = false;
                if (isEnlarged) img.style.cursor = 'grab';
                }
            });

            // Optional: zoom with wheel while enlarged
            img.addEventListener('wheel', (e) => {
                if (!isEnlarged) return;
                e.preventDefault();
                // For simplicity, we won't scale further — just pan
                // (You could add dynamic scaling if needed)
            });
            });
        </script>
    

Lightbox with pan and zoom within the original webpage

Click an image to enlarge it. Drag to pan while enlarged.

holiday
holiday
holiday
holiday
holiday
holiday
holiday
holiday
holiday
holiday
code:
        <div class="gallery3" id="gallery3">
            <div><img src="../images/1.jpg" class="thumbnail" alt="holiday" width="100%" height="100%"/></div>
            <div><img src="../images/2.jpg" class="thumbnail" alt="holiday" width="100%" height="100%"/></div>
            <div><img src="../images/3.jpg" class="thumbnail" alt="holiday" width="100%" height="100%"/></div>
            <div><img src="../images/4.jpg" class="thumbnail" alt="holiday" width="100%" height="100%"/></div>
            <div><img src="../images/5.jpg" class="thumbnail" alt="holiday" width="100%" height="100%"/></div>
            <div><img src="../images/1a.jpg" class="thumbnail" alt="holiday" width="100%" height="100%"/></div>
            <div><img src="../images/2a.jpg" class="thumbnail" alt="holiday" width="100%" height="100%"/></div>
            <div><img src="../images/3a.jpg" class="thumbnail" alt="holiday" width="100%" height="100%"/></div>
            <div><img src="../images/4a.jpg" class="thumbnail" alt="holiday" width="100%" height="100%"/></div>
            <div><img src="../images/5a.jpg" class="thumbnail" alt="holiday" width="100%" height="100%"/></div>
        </div>
        <div id="lightbox1" class="lightbox1 hidden1">
            <div class="lightbox-content1">
                <button class="close1">×</button>
                <div class="pan-container1">
                <img src="image.jpg" id="zoom-image1" alt="Zoomable1">
                </div>
            </div>
        </div>
        <style>
            .gallery3 {display: flex; flex-wrap: wrap; gap: 2vw; margin: 2vw 0;}
            .thumbnail {width: 16vw; height: 28vh; cursor: pointer; position: relative;max-width: 20vw;}
            .lightbox1 {position: fixed; top: 0; left: 0; width: 50%; height: 50%; background: rgba(0, 0, 0, 0.8); z-index: 1000;  display: flex; justify-content: center; align-items: center;}
            .lightbox1.hidden1 {display: none;}
            .lightbox-content1 {position: relative; max-width: 90vw; max-height: 90vh; overflow: hidden;}
            .pan-container1 {overflow: hidden; cursor: grab;  max-width: 100%; max-height: 100%; display: flex; justify-content: center; align-items: center; }
            .pan-container1 img {transition: transform 0.1s; max-width: none; max-height: none; }
            .close1 {position: absolute; top: 1vw; right: 1.5vw; background: white; border: none; font-size: 1.5vw; cursor: pointer; border-radius: 50%;  width: 2.2vw;   height: 2.4vh; display: flex; align-items: center; justify-content: center; }
            #zoom-image1{width: 40vw; height: 60vh; top: 2vw; left: 4vw;}
        </style>
        <script>
            document.querySelectorAll('.thumbnail').forEach(thumb => {
                thumb.addEventListener('click', () => {
                    const lightbox = document.getElementById('lightbox1');
                    const img = lightbox.querySelector('#zoom-image1');
                    img.src = thumb.src;
                    lightbox.classList.remove('hidden1');
                    initPanZoom(img);
                });
            });

            document.querySelector('.close1').addEventListener('click', () => {
            document.getElementById('lightbox1').classList.add('hidden1');
            });

            // Close lightbox on backdrop click (optional but user-friendly)
            document.getElementById('lightbox1').addEventListener('click', (e) => {
                if (e.target === e.currentTarget) {
                    e.currentTarget.classList.add('hidden1');
                }
            });

            function initPanZoom(img) {
                const container = img.parentElement;
                let scale = 1;
                let posX = 0, posY = 0;
                let isPanning = false;
                let startX, startY, startPX, startPY;

                // Reset state
                img.style.transform = 'translate(0,0) scale(1)';
                scale = 1;
                posX = 0;
                posY = 0;
                container.style.cursor = 'grab';

                // Disable image drag (prevents native drag behavior)
                img.draggable = false;

                // --- Wheel Zoom ---
                container.addEventListener('wheel', (e) => {
                    e.preventDefault();
                    const delta = e.deltaY > 0 ? 0.9 : 1.1;
                    const newScale = Math.min(Math.max(0.5, scale * delta), 5);

                    // Compute zoom point relative to container
                    const containerRect = container.getBoundingClientRect();
                    const mouseX = e.clientX - containerRect.left;
                    const mouseY = e.clientY - containerRect.top;

                    // Adjust pan so zoom centers on mouse
                    const zoomFactor = newScale / scale;
                    posX = mouseX - (mouseX - posX) * zoomFactor;
                    posY = mouseY - (mouseY - posY) * zoomFactor;

                    scale = newScale;
                    applyTransform();
                }, { passive: false });

                // --- Mouse Panning ---
                container.addEventListener('mousedown', (e) => {
                    if (e.button !== 0) return; // only left mouse
                    isPanning = true;
                    startX = e.clientX;
                    startY = e.clientY;
                    startPX = posX;
                    startPY = posY;
                    container.style.cursor = 'grabbing';
                    e.preventDefault(); // Prevent text/image selection
                });

                window.addEventListener('mousemove', (e) => {
                    if (!isPanning) return;
                    posX = startPX + (e.clientX - startX);
                    posY = startPY + (e.clientY - startY);
                    applyTransform();
                });

                window.addEventListener('mouseup', () => {
                    isPanning = false;
                    container.style.cursor = 'grab';
                });

                // --- Touch Panning ---
                let touchStartX, touchStartY;

                container.addEventListener('touchstart', (e) => {
                    if (e.touches.length === 1) {
                    isPanning = true;
                    touchStartX = e.touches[0].clientX;
                    touchStartY = e.touches[0].clientY;
                    startPX = posX;
                    startPY = posY;
                    e.preventDefault();
                    }
                });

                container.addEventListener('touchmove', (e) => {
                    if (isPanning && e.touches.length === 1) {
                    const dx = e.touches[0].clientX - touchStartX;
                    const dy = e.touches[0].clientY - touchStartY;
                    posX = startPX + dx;
                    posY = startPY + dy;
                    applyTransform();
                    e.preventDefault();
                    }
                });

                container.addEventListener('touchend', () => {
                    isPanning = false;
                });

                // Apply CSS transform
                function applyTransform() {
                    img.style.transform = `translate(${posX}px, ${posY}px) scale(${scale})`;
                }
            }
        </script>